use std::ops::ControlFlow;

use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::eager_or_lazy::switch_to_lazy_eval;
use clippy_utils::higher::VecArgs;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::res::MaybeDef;
use clippy_utils::source::snippet_with_context;
use clippy_utils::ty::{expr_type_is_certain, implements_trait};
use clippy_utils::visitors::for_each_expr;
use clippy_utils::{
    contains_return, is_default_equivalent, is_default_equivalent_call, last_path_segment, peel_blocks, sym,
};
use rustc_errors::Applicability;
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::{Span, Symbol};
use {rustc_ast as ast, rustc_hir as hir};

use super::{OR_FUN_CALL, UNWRAP_OR_DEFAULT};

/// Checks for the `OR_FUN_CALL` lint.
pub(super) fn check<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &hir::Expr<'_>,
    method_span: Span,
    name: Symbol,
    receiver: &'tcx hir::Expr<'_>,
    args: &'tcx [hir::Expr<'_>],
    msrv: Msrv,
) {
    if let [arg] = args {
        let inner_arg = peel_blocks(arg);
        for_each_expr(cx, inner_arg, |ex| {
            // `or_fun_call` lint needs to take nested expr into account,
            // but `unwrap_or_default` lint doesn't, we don't want something like:
            // `opt.unwrap_or(Foo { inner: String::default(), other: 1 })` to get replaced by
            // `opt.unwrap_or_default()`.
            let is_nested_expr = ex.hir_id != inner_arg.hir_id;

            let is_triggered = match ex.kind {
                hir::ExprKind::Call(fun, fun_args) => {
                    let inner_fun_has_args = !fun_args.is_empty();
                    let fun_span = if inner_fun_has_args || is_nested_expr {
                        None
                    } else {
                        Some(fun.span)
                    };
                    (!inner_fun_has_args
                        && !is_nested_expr
                        && check_unwrap_or_default(cx, name, receiver, fun, Some(ex), expr.span, method_span, msrv))
                        || check_or_fn_call(cx, name, method_span, receiver, arg, None, expr.span, fun_span)
                },
                hir::ExprKind::Path(..) | hir::ExprKind::Closure(..) if !is_nested_expr => {
                    check_unwrap_or_default(cx, name, receiver, ex, None, expr.span, method_span, msrv)
                },
                hir::ExprKind::Index(..) | hir::ExprKind::MethodCall(..) => {
                    check_or_fn_call(cx, name, method_span, receiver, arg, None, expr.span, None)
                },
                _ => false,
            };

            if is_triggered {
                ControlFlow::Break(())
            } else {
                ControlFlow::Continue(())
            }
        });
    }

    // `map_or` takes two arguments
    if let [arg, lambda] = args {
        let inner_arg = peel_blocks(arg);
        for_each_expr(cx, inner_arg, |ex| {
            let is_top_most_expr = ex.hir_id == inner_arg.hir_id;
            match ex.kind {
                hir::ExprKind::Call(fun, fun_args) => {
                    let fun_span = if fun_args.is_empty() && is_top_most_expr {
                        Some(fun.span)
                    } else {
                        None
                    };
                    if check_or_fn_call(cx, name, method_span, receiver, arg, Some(lambda), expr.span, fun_span) {
                        return ControlFlow::Break(());
                    }
                },
                hir::ExprKind::MethodCall(..) => {
                    if check_or_fn_call(cx, name, method_span, receiver, arg, Some(lambda), expr.span, None) {
                        return ControlFlow::Break(());
                    }
                },
                _ => {},
            }
            ControlFlow::Continue(())
        });
    }
}

/// Checks for `unwrap_or(T::new())`, `unwrap_or(T::default())`,
/// `or_insert(T::new())` or `or_insert(T::default())`.
/// Similarly checks for `unwrap_or_else(T::new)`, `unwrap_or_else(T::default)`,
/// `or_insert_with(T::new)` or `or_insert_with(T::default)`.
#[expect(clippy::too_many_arguments)]
fn check_unwrap_or_default(
    cx: &LateContext<'_>,
    name: Symbol,
    receiver: &hir::Expr<'_>,
    fun: &hir::Expr<'_>,
    call_expr: Option<&hir::Expr<'_>>,
    span: Span,
    method_span: Span,
    msrv: Msrv,
) -> bool {
    let receiver_ty = cx.typeck_results().expr_ty_adjusted(receiver).peel_refs();

    // Check MSRV, but only for `Result::unwrap_or_default`
    if receiver_ty.is_diag_item(cx, sym::Result) && !msrv.meets(cx, msrvs::RESULT_UNWRAP_OR_DEFAULT) {
        return false;
    }

    if !expr_type_is_certain(cx, receiver) {
        return false;
    }

    let is_new = |fun: &hir::Expr<'_>| {
        if let hir::ExprKind::Path(ref qpath) = fun.kind {
            let path = last_path_segment(qpath).ident.name;
            matches!(path, sym::new)
        } else {
            false
        }
    };

    let output_type_implements_default = |fun| {
        let fun_ty = cx.typeck_results().expr_ty(fun);
        if let ty::FnDef(def_id, args) = fun_ty.kind() {
            let output_ty = cx.tcx.fn_sig(def_id).instantiate(cx.tcx, args).skip_binder().output();
            cx.tcx
                .get_diagnostic_item(sym::Default)
                .is_some_and(|default_trait_id| implements_trait(cx, output_ty, default_trait_id, &[]))
        } else {
            false
        }
    };

    let sugg = match (name, call_expr.is_some()) {
        (sym::unwrap_or, true) | (sym::unwrap_or_else, false) => sym::unwrap_or_default,
        (sym::or_insert, true) | (sym::or_insert_with, false) => sym::or_default,
        _ => return false,
    };

    let Some(suggested_method_def_id) = receiver_ty.ty_adt_def().and_then(|adt_def| {
        cx.tcx
            .inherent_impls(adt_def.did())
            .iter()
            .flat_map(|impl_id| cx.tcx.associated_items(impl_id).filter_by_name_unhygienic(sugg))
            .find_map(|assoc| {
                if assoc.is_method() && cx.tcx.fn_sig(assoc.def_id).skip_binder().inputs().skip_binder().len() == 1 {
                    Some(assoc.def_id)
                } else {
                    None
                }
            })
    }) else {
        return false;
    };
    let in_sugg_method_implementation = {
        matches!(
            suggested_method_def_id.as_local(),
            Some(local_def_id) if local_def_id == cx.tcx.hir_get_parent_item(receiver.hir_id).def_id
        )
    };
    if in_sugg_method_implementation {
        return false;
    }

    // `.unwrap_or(vec![])` is as readable as `.unwrap_or_default()`. And if the expression is a
    // non-empty `Vec`, then it will not be a default value anyway. Bail out in all cases.
    if call_expr.and_then(|call_expr| VecArgs::hir(cx, call_expr)).is_some() {
        return false;
    }

    // needs to target Default::default in particular or be *::new and have a Default impl
    // available
    if (is_new(fun) && output_type_implements_default(fun))
        || match call_expr {
            Some(call_expr) => is_default_equivalent(cx, call_expr),
            None => is_default_equivalent_call(cx, fun, None) || closure_body_returns_empty_to_string(cx, fun),
        }
    {
        span_lint_and_sugg(
            cx,
            UNWRAP_OR_DEFAULT,
            method_span.with_hi(span.hi()),
            format!("use of `{name}` to construct default value"),
            "try",
            format!("{sugg}()"),
            Applicability::MachineApplicable,
        );

        true
    } else {
        false
    }
}

/// Checks for `*or(foo())`.
#[expect(clippy::too_many_arguments)]
fn check_or_fn_call<'tcx>(
    cx: &LateContext<'tcx>,
    name: Symbol,
    method_span: Span,
    self_expr: &hir::Expr<'_>,
    arg: &'tcx hir::Expr<'_>,
    // `Some` if fn has second argument
    second_arg: Option<&hir::Expr<'_>>,
    span: Span,
    // None if lambda is required
    fun_span: Option<Span>,
) -> bool {
    // (path, fn_has_argument, methods, suffix)
    const KNOW_TYPES: [(Symbol, bool, &[Symbol], &str); 7] = [
        (sym::BTreeEntry, false, &[sym::or_insert], "with"),
        (sym::HashMapEntry, false, &[sym::or_insert], "with"),
        (
            sym::Option,
            false,
            &[sym::map_or, sym::ok_or, sym::or, sym::unwrap_or],
            "else",
        ),
        (sym::Option, false, &[sym::get_or_insert], "with"),
        (sym::Option, true, &[sym::and], "then"),
        (sym::Result, true, &[sym::map_or, sym::or, sym::unwrap_or], "else"),
        (sym::Result, true, &[sym::and], "then"),
    ];

    if KNOW_TYPES.iter().any(|k| k.2.contains(&name))
        && switch_to_lazy_eval(cx, arg)
        && !contains_return(arg)
        && let self_ty = cx.typeck_results().expr_ty(self_expr)
        && let Some(&(_, fn_has_arguments, _, suffix)) = KNOW_TYPES
            .iter()
            .find(|&&i| self_ty.is_diag_item(cx, i.0) && i.2.contains(&name))
    {
        let ctxt = span.ctxt();
        let mut app = Applicability::HasPlaceholders;
        let sugg = {
            let (snippet_span, use_lambda) = match (fn_has_arguments, fun_span) {
                (false, Some(fun_span)) => (fun_span, false),
                _ => (arg.span, true),
            };

            let snip = snippet_with_context(cx, snippet_span, ctxt, "..", &mut app).0;
            let snip = if use_lambda {
                let l_arg = if fn_has_arguments { "_" } else { "" };
                format!("|{l_arg}| {snip}")
            } else {
                snip.into_owned()
            };

            if let Some(f) = second_arg {
                let f = snippet_with_context(cx, f.span, ctxt, "..", &mut app).0;
                format!("{snip}, {f}")
            } else {
                snip
            }
        };
        let span_replace_word = method_span.with_hi(span.hi());
        span_lint_and_sugg(
            cx,
            OR_FUN_CALL,
            span_replace_word,
            format!("function call inside of `{name}`"),
            "try",
            format!("{name}_{suffix}({sugg})"),
            app,
        );
        true
    } else {
        false
    }
}

fn closure_body_returns_empty_to_string(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> bool {
    if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = e.kind {
        let body = cx.tcx.hir_body(body);

        if body.params.is_empty()
            && let hir::Expr { kind, .. } = &body.value
            && let hir::ExprKind::MethodCall(hir::PathSegment { ident, .. }, self_arg, [], _) = kind
            && ident.name == sym::to_string
            && let hir::Expr { kind, .. } = self_arg
            && let hir::ExprKind::Lit(lit) = kind
            && let ast::LitKind::Str(rustc_span::sym::empty, _) = lit.node
        {
            return true;
        }
    }

    false
}
